Skip to content

Include failure context in splice events#4514

Open
jkczyz wants to merge 20 commits intolightningdevkit:mainfrom
jkczyz:2026-03-splice-rbf-fail-event
Open

Include failure context in splice events#4514
jkczyz wants to merge 20 commits intolightningdevkit:mainfrom
jkczyz:2026-03-splice-rbf-fail-event

Conversation

@jkczyz
Copy link
Copy Markdown
Contributor

@jkczyz jkczyz commented Mar 26, 2026

When a splice negotiation round fails, the user needs to know why it failed and what they can do about it. Previously, SpliceFailed gave no indication of the failure cause and didn't return the contribution, making it difficult to retry.

This PR adds failure context to the splice events so users can make informed retry decisions:

  • NegotiationFailureReason indicates what went wrong — peer disconnect, counterparty abort, invalid contribution, insufficient feerate, channel closing, etc. Each variant documents how to resolve it.
  • The FundingContribution from the failed round is returned in the event. Users can feed it back to funding_contributed to retry as-is, or inspect it to understand which inputs, outputs, and feerate were used when constructing a new contribution.
  • SplicePending and SpliceFailed are renamed to SpliceNegotiated and SpliceNegotiationFailed to reflect that each negotiation round (initial or RBF) independently resolves to one of these two outcomes.
  • DiscardFunding is now emitted before SpliceNegotiationFailed so wallet inputs are unlocked before the failure handler runs. Otherwise, a retry during SpliceNegotiationFailed handling would leave inputs locked, and the subsequent DiscardFunding would incorrectly unlock inputs committed to the new attempt.

Additionally, SpliceFundingFailed internals are simplified:

  • Dead funding_txo and channel_type fields are removed.
  • An unreachable was_negotiated check is removed from the contribution pop.
  • Inputs and outputs are derived from FundingContribution via a unified splice_funding_failed_for! macro, replacing the old approach of extracting them from FundingNegotiation variants. Unused conversion methods are removed.

Also fixes a bug in into_unique_contributions where outputs were compared by full TxOut equality instead of script_pubkey. This could fail to filter change outputs that reused the same address but had different values across RBF rounds.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Mar 26, 2026

🎉 This PR is now ready for review!
Please choose at least one reviewer by assigning them on the right bar.
If no reviewers are assigned within 10 minutes, I'll automatically assign one.
Once the first reviewer has submitted a review, a second will be assigned if required.

@jkczyz jkczyz self-assigned this Mar 26, 2026
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch 2 times, most recently from 333980d to c2cad50 Compare March 28, 2026 00:08
@jkczyz jkczyz requested review from TheBlueMatt and wpaulino March 28, 2026 00:10
/// sent an invalid message). Retry by calling [`ChannelManager::splice_channel`].
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
NegotiationError,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note we could have this wrap AbortReason -- we had an internal NegotiationError struct that already wrapped this along with contributions -- but it would require making AbortReason public. Not sure if we want to expose all its variants.

fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self {
match self {
QuiescentError::FailSplice(_, ref mut r) => *r = reason,
_ => debug_assert!(false, "Expected FailSplice variant"),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Little unfortunate we need to do this, but I couldn't find a better way other than passing NegotiationFailureReason to abandon_quiescent_action and quiescent_action_into_error, which doesn't seem right since they could be for future QuiescentAction variants.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does feel like quiescent_action_into_error could take the NegotiationFailureReason and we could also use NegotiationFailureReason for dual-funding (the DiscardFunding variant) but this seems fine.

Comment on lines -1646 to -1649
/// The outpoint of the channel's splice funding transaction, if one was created.
abandoned_funding_txo: Option<OutPoint>,
/// The features that this channel will operate with, if available.
channel_type: Option<ChannelTypeFeatures>,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't feel like there was much value to these.

  • abandoned_funding_txo would be set only if there's a failure after the transaction was negotiated but before signed.
  • In the future, when channel_type can change, maybe it is just part of FundingContribution? Could also keep it if you prefer.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz marked this pull request as ready for review April 2, 2026 17:04
Comment on lines +3185 to 3193
impl QuiescentError {
fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self {
match self {
QuiescentError::FailSplice(_, ref mut r) => *r = reason,
_ => debug_assert!(false, "Expected FailSplice variant"),
}
self
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: The debug_assert!(false) path is only reachable in non-production builds if a caller passes a non-FailSplice variant. Currently the only caller passes through quiescent_action_into_error which always produces FailSplice for production code (only DoNothing in test builds). But debug_assert!(false) will silently return self unchanged in release builds — consider using debug_assert! with a more specific message about which variant was unexpected, or restructuring so the method only accepts FailSplice to make this impossible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +6943 to +6947
let is_initiator = pending_splice
.funding_negotiation
.take()
.map(|negotiation| negotiation.is_initiator())
.unwrap_or(false);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_initiator fallback when funding_negotiation is None defaults to false, which means splice_funding_failed_for! will return None for non-initiators (since the macro has None if !$is_initiator => None). This means if the funding negotiation was already taken before reset_pending_splice_state is called and the node was actually the initiator, the splice failure event would be silently dropped.

Is funding_negotiation ever None when reset_pending_splice_state is called? If so, defaulting to false may suppress failure events for the initiator. If not, this should be a debug_assert or .expect() to catch violations of that invariant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added debug assertions.

Comment on lines +6989 to +6993
let is_initiator = pending_splice
.funding_negotiation
.as_ref()
.map(|negotiation| negotiation.is_initiator())
.unwrap_or(false);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern here: funding_negotiation.as_ref() being None causes is_initiator to default to false, which will cause the splice_funding_failed_for! macro to return None for non-initiators. Since maybe_splice_funding_failed is the read-only version used during serialization, silently returning None means the splice failure events won't be persisted and the user won't learn about the failure after restart.

Consider whether funding_negotiation can actually be None in this path, and if not, using expect to enforce the invariant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added debug assertions.

@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented Apr 2, 2026

I've completed a thorough review of the entire PR diff, cross-referencing with my prior review comments and the underlying code. All previously flagged issues remain applicable. I found no significant new bugs or security vulnerabilities.

Review Summary

Previously flagged issues (still applicable):

  1. lightning/src/ln/channel.rs:3188with_negotiation_failure_reason silently no-ops in release for non-FailSplice variants due to debug_assert!(false).
  2. lightning/src/ln/channel.rs:7267is_initiator defaults to false when funding_negotiation is None, potentially suppressing initiator failure events.
  3. lightning/src/ln/channel.rs:7317 — Same is_initiator defaulting issue in maybe_splice_funding_failed (serialization path).
  4. lightning/src/ln/channel.rs:14025 — Duplicate quiescent action returns NegotiationFailureReason::Unknown because .with_negotiation_failure_reason() is not chained.

Cross-cutting concerns:

  • Forward compatibility of NegotiationFailureReason: All 8 variant IDs are even (0, 2, 4, ..., 14). In impl_writeable_tlv_based_enum_upgradable!, unknown even variants return Err(UnknownRequiredFeature). Since the reason field uses upgradable_option, the error from an unknown even variant propagates through the ? operator in _decode_tlv!, failing the entire event deserialization and preventing ChannelManager loading. Future variants should use odd IDs for forward compatibility, or at least the next variant added should be odd so older nodes can gracefully degrade to Unknown.

  • Changelog completeness: The pending changelog mentions feerate() and inputs() accessors on FundingContribution, but the PR also adds outputs(), change_output(), and value_added() as new public API methods that are not listed.

Each splice negotiation round can fail for different reasons, but
Event::SpliceFailed previously gave no indication of what went wrong.
Add a NegotiationFailureReason enum so users can distinguish failures
and take appropriate action (e.g., retry with a higher feerate vs.
wait for the channel to become usable).

The reason is determined at each channelmanager emission site based on
context rather than threaded through channel.rs internals, since the
channelmanager knows the triggering context (disconnect, tx_abort,
shutdown, etc.) while channel.rs functions like abandon_quiescent_action
handle both splice and non-splice quiescent actions.

The one exception is QuiescentError::FailSplice, which carries a reason
alongside the SpliceFundingFailed. This is appropriate because FailSplice
is already splice-specific, and the channel.rs code that constructs it
(e.g., contribution validation, feerate checks) knows the specific
failure cause. A with_negotiation_failure_reason method on QuiescentError
allows callers to override the default when needed.

Older serializations that lack the reason field default to Unknown via
default_value in deserialization. The persistence reload path uses
PeerDisconnected since a reload implies the peer connection was lost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from c2cad50 to c469423 Compare April 3, 2026 01:25
@jkczyz
Copy link
Copy Markdown
Contributor Author

jkczyz commented Apr 3, 2026

Rebased

@@ -4128,6 +4118,16 @@ impl<
},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Unlike handle_quiescent_error (line ~6619), this site unconditionally emits DiscardFunding even when contributed_inputs and contributed_outputs are both empty (which happens for the initiator when all inputs are committed to prior rounds). The same inconsistency exists at lines ~4417, ~4920, ~11853, ~12012, ~12180, ~12329, ~15448, and ~18122.

Consider adding the same guard here:

if !splice_funding_failed.contributed_inputs.is_empty() || !splice_funding_failed.contributed_outputs.is_empty() {
    // emit DiscardFunding
}

Or, equivalently, have the macro return None for the initiator when there are no unique contributions (matching the non-initiator behavior) and emit SpliceNegotiationFailed without DiscardFunding in those cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by making SpliceFundingFailed consumable into Option-al parts rather than exposing fields. The into_parts method now checks for empty contributions.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 84.69945% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.10%. Comparing base (2adb690) to head (c469423).

Files with missing lines Patch % Lines
lightning/src/ln/channelmanager.rs 79.61% 21 Missing ⚠️
lightning/src/ln/funding.rs 40.00% 3 Missing ⚠️
lightning/src/ln/channel.rs 96.55% 1 Missing and 1 partial ⚠️
lightning/src/events/mod.rs 85.71% 1 Missing ⚠️
lightning/src/ln/functional_test_utils.rs 90.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4514   +/-   ##
=======================================
  Coverage   87.09%   87.10%           
=======================================
  Files         163      163           
  Lines      108856   108776   -80     
  Branches   108856   108776   -80     
=======================================
- Hits        94808    94744   -64     
+ Misses      11563    11545   -18     
- Partials     2485     2487    +2     
Flag Coverage Δ
fuzzing 40.25% <30.63%> (+0.05%) ⬆️
tests 86.21% <84.69%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

jkczyz and others added 2 commits April 2, 2026 22:13
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from c469423 to 4f6b1a6 Compare April 3, 2026 13:42
Unknown future variants (using odd IDs) will deserialize as Unknown
instead of blocking ChannelManager loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from 4f6b1a6 to 0609565 Compare April 3, 2026 15:33
@jkczyz
Copy link
Copy Markdown
Contributor Author

jkczyz commented Apr 3, 2026

  • Forward compatibility of NegotiationFailureReason: Uses impl_writeable_tlv_based_enum! with all-even variant IDs. A future variant added by a newer LDK version would cause UnknownRequiredFeature on deserialization, blocking ChannelManager loading on older versions. Consider impl_writeable_tlv_based_enum_upgradable! or documenting the constraint.

Changed to use impl_writeable_tlv_based_enum_upgradable and changed the reason from default_value to upgradable_option, defaulting to NegotiationFailureReason::Unknown.

@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from 0609565 to 44ecc06 Compare April 3, 2026 15:39
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 3rd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 3rd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 4th Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 4th Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

/// [`ChannelManager::splice_channel`], or wait for the counterparty to initiate.
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
CounterpartyAborted,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the "Message data" in tx_abort not a string that we can include here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
PeerDisconnected,
/// The counterparty aborted the negotiation by sending `tx_abort`. Retry by calling
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost each docstring says "Retry by calling splice_channel". That doesn't seem like particularly helpful advice especially in cases where the counterparty rejected implying just trying again is unlikely to fix it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it really depends on the reason. They may just not like our fee rate, for instance. Protocol violations result in a disconnect rather than an abort. Hmm... that makes me wonder if retrying on disconnect is always the right approach... Presumably protocol violations are an LDK bug.

/// Adjust the contribution and retry via [`ChannelManager::splice_channel`].
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
ContributionInvalid,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem like some of these could use more details (error message string?). As-is given the recommendation for ~all enum variants is "retry by calling splice_channel again" its not really clear to me why we should bother with an enum - an enum is useful if the downstream code has different handling for different cases, but here that doesn't really seem likely/possible. If we can't expose more details that are useful for automated action, we could probably do with an enum with three variants and a developer-readable string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the docs could be improved here. Maybe re-calling splice_channel isn't always necessary? In some cases, calling funding_contributed at a later time may be sufficient. We are providing the FundingContribution after all.

So it does seem like different actions are required depending on the variant.

  • PeerDisconnected: retry upon re-connection
  • ChannelNotReady: retry when channel is in a usable state (assuming it's not closing)
  • FeeRateTooLow: retry with a higher fee rate
  • ContributionInvalid: retry with a re-computed contribution
  • NegotiationError: probably not retryable as it indicates a counterparty error
  • CounterpartyAborted: might be able to retry but would need to examine the tx_abort message (e.g., counterparty doesn't like your fee rate).

We could have a top-level Retryable that holds one of these instead, if you prefer? We could go as far as only putting the FundingContribution in applicable sub-variants. Though a user may want to see it for other variants.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume ChannelNotReady is really "you can't retry" (why were you splicing an unopened channel anyway? but probably its closing), FeeRateTooLow and ContributionInvalid are really "retry now with a fresh splice_channel call as that'll tell you what the new required feerate is to do a fresh rbf from scracth or will fix your contribution, hopefully". So it does seem like its basically "retry with a fresh splice_channel call, or not", with the one maybe-exception of PeerDisconnected? We could leave the enum and include a function that returns "retryable or not", but I'm not really sure why we shouldn't just return a string+bool pair (in a struct) - do we anticipate downstream code will have any different behavior based on anything beyond "should I retry this" or is it just gonna log/print an error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not other than when / how should you retry in case of the PeerDisconnected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose given we don't know when the peer will reconnect, we might as well just process it the same and call splice_channel again upon re-connection. Otherwise, we'd need to make sure not to process the associated DiscardFunding which would mean locking up the UTXOs indefinitely. Instead, the client can simply always queue up a "retry when connected".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. Updated to have an is_retryable method that the counterparty calls rather than examining the variant. Would prevent allocating strings in many cases.

if let Some(splice_funding_failed) = splice_funding_failed {
let (funding_info, contribution) = splice_funding_failed.into_parts();
let mut pending_events = self.pending_events.lock().unwrap();
if let Some(funding_info) = funding_info {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document which comes first and maybe test it more exhaustively - eg in get_and_clear_pending_events assert the order if both events appear for the same channel?

jkczyz and others added 16 commits April 6, 2026 23:17
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the abandoned_funding_txo and channel_type fields on
Event::SpliceFailed with an Option<FundingContribution> from the failed
round. Users can feed this back to funding_contributed to retry or use
it to inform a fresh attempt via splice_channel.

Also makes FundingContribution::feerate() public so users can inspect
the feerate when deciding whether to retry or bump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverse the event ordering at all emission sites so that
Event::DiscardFunding is emitted before Event::SpliceFailed. If the
user retries the splice when handling SpliceFailed, the contributed
inputs would still be locked. A subsequent DiscardFunding would then
incorrectly unlock inputs that are now committed to the new attempt.
Emitting DiscardFunding first avoids this by ensuring inputs are
unlocked before any retry occurs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename Event::SplicePending to Event::SpliceNegotiated and
Event::SpliceFailed to Event::SpliceNegotiationFailed. These names
better reflect the per-round semantics: each negotiation attempt
resolves to one of these two outcomes, independent of the overall
splice lifecycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The was_negotiated check is unnecessary because reset_pending_splice_state
only runs when funding_negotiation is present, meaning
on_tx_signatures_exchange hasn't been called yet. Since the feerate is
only recorded in last_funding_feerate_sat_per_1000_weight during
on_tx_signatures_exchange, the current round's feerate can never match
it. So the contribution can always be unconditionally popped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Filter outputs by script_pubkey rather than full TxOut equality. Outputs
reusing the same address as a prior round are still considered committed
even if the value differs (e.g., different change amounts across RBF
rounds with different feerates).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the maybe_create_splice_funding_failed! macro and
splice_funding_failed_for method with a unified splice_funding_failed_for!
macro that derives contributed inputs and outputs from the
FundingContribution rather than extracting them from the negotiation
state.

Callers pass ident parameters for which PendingSplice filtering methods
to use: contributed_inputs/contributed_outputs when the current round's
contribution has been popped or was never pushed, and
prior_contributed_inputs/prior_contributed_outputs for the read-only
persistence path where the contribution is cloned instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gotiationContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hods

Now that splice_funding_failed_for! derives inputs and outputs from
FundingContribution directly, remove the unused NegotiationError struct
and into_negotiation_error methods from the interactive tx types, along
with the into/to_contributed_inputs_and_outputs methods on
ConstructedTransaction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from 44ecc06 to 48f950f Compare April 7, 2026 04:22
Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I think the API lgtm now

}

impl NegotiationFailureReason {
/// Whether the splice negotiation may be retried on this channel. When `true`, call
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
/// Whether the splice negotiation may be retried on this channel. When `true`, call
/// Whether the splice negotiation is likely to succeed if retried on this channel. When `true`, call

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants